package main;

/**
 * @author Ken "David" McClain
 * @author Zach Turk
 */

import java.awt.*;
import java.awt.event.*;
import java.nio.IntBuffer;
import java.util.LinkedList;
import java.util.List;

import javax.media.opengl.*;
import javax.media.opengl.awt.GLCanvas;
import javax.media.opengl.glu.GLU;
import javax.swing.*;

import shape.*;

import com.jogamp.common.nio.Buffers;
import com.jogamp.opengl.util.FPSAnimator;

public class Main implements GLEventListener {
	private static JFrame appFrame;
	private static SceneGraphNode sceneGraphRoot;
	
	// Fields used for "Picked" functionality
	private static Point pickedPoint;
	private static boolean pickNextFrame;
	private static double left, right, top, bottom;
	
	private static double height, width; // Screen height/width (in pixels). used by mouseDragged()
	private static List<SceneGraphNode> pickedNodes = new LinkedList<SceneGraphNode>();
	
	// Flag for when shift key is held down.
	private static boolean shiftDown = false;
	private static boolean ctrlDown = false;
	
	// All menu items
	private JRadioButtonMenuItem jrbSelect   = new JRadioButtonMenuItem("Select Shapes (hold SHIFT to rotate, Ctrl to scale)");
	private JRadioButtonMenuItem jrbTriangle = new JRadioButtonMenuItem("Add Triangle");
	private JRadioButtonMenuItem jrbEllipse  = new JRadioButtonMenuItem("Add Ellipse");
	private JRadioButtonMenuItem jrbSquare   = new JRadioButtonMenuItem("Add Square");
	private JRadioButtonMenuItem jrbCompound = new JRadioButtonMenuItem("Add Compound Shape");
	private JRadioButtonMenuItem jrbDelete   = new JRadioButtonMenuItem("Remove Shape");
	
	// Flag for when "Remove" is chosen.
	private static boolean deleteNextPicked = false;
	
	private static float SCALE = 0.5f;
	
	public static void main(String[] args) {
		GLProfile.initSingleton();
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				(new Main()).createAndShowGUI();
			}
		});
	}
	
	private JMenuBar buildMenus() {
		// Builds and returns menu bar.
		JMenuBar jmb = new JMenuBar();
		ButtonGroup bg = new ButtonGroup();
		bg.add(jrbSelect);
		bg.add(jrbTriangle);
		bg.add(jrbEllipse);
		bg.add(jrbSquare);
		bg.add(jrbCompound);
		bg.add(jrbDelete);
		jmb.add(jrbSelect);
		jmb.add(jrbTriangle);
		jmb.add(jrbEllipse);
		jmb.add(jrbSquare);
		jmb.add(jrbCompound);
		jmb.add(jrbDelete);
		
		// Start out in "select" mode
		jrbSelect.setSelected(true);
		
		return jmb;
	}
	
	private void createAndShowGUI() {
		// Fix for background flickering
		// System.setProperty("sun.awt.noerasebackground", "true");
		
		GLProfile profile = GLProfile.getDefault();
		GLCapabilities capabilities = new GLCapabilities(profile);
		final GLCanvas canvas = new GLCanvas(capabilities);
		canvas.addGLEventListener(this);
		canvas.addMouseListener(new MouseAdapter() {
			public void mousePressed(MouseEvent e) {
				// Get JOGL coordinates based on mouse position.
				float x = getMouseX(e);
				float y = getMouseY(e);
				
				// Check which menu item is selected, create objects.
				if (jrbTriangle.isSelected()) {
					Triangle triangle = new Triangle(true);
					triangle.translateX = x;
					triangle.translateY = y;
					sceneGraphRoot.addChild(triangle);
					//jrbSelect.setSelected(true); // Switch to "Select"
				
				} else if (jrbEllipse.isSelected()) {
					Ellipse ellipse = new Ellipse(true);
					ellipse.scale = .5f;
					ellipse.translateX = x;
					ellipse.translateY = y;
					sceneGraphRoot.addChild(ellipse);
					//jrbSelect.setSelected(true); // Switch to "Select"
				
				} else if (jrbSquare.isSelected()) {
					Square square = new Square(true);
					square.translateX = x;
					square.translateY = y;
					sceneGraphRoot.addChild(square);
					//jrbSelect.setSelected(true); // Switch to "Select"
				
				} else if (jrbCompound.isSelected()) {
					Compound compound = new Compound();
					compound.scale = 0.5f;
					compound.translateX = x;
					compound.translateY = y;
					sceneGraphRoot.addChild(compound);
					//jrbSelect.setSelected(true); // Switch to "Select"
					
				} else if (jrbDelete.isSelected()) {
					deleteNextPicked = true;
				}
				
				pickNextFrame = true;
				pickedPoint = new Point(e.getX(), e.getY());
				
			}
			public void mouseReleased(MouseEvent e) {
				// Fixes "ghost" movement of shapes after they are released
				pickedNodes.clear();
			}
		});
		
		canvas.addKeyListener(new KeyAdapter() {
			public void keyPressed(KeyEvent e) {
				shiftDown = e.isShiftDown();
				ctrlDown  = e.isControlDown();
			}
			public void keyReleased(KeyEvent e) {
				shiftDown = false;
				ctrlDown = false;
			}
		});
		
		canvas.addMouseMotionListener(new MouseAdapter() {
			public void mouseDragged(MouseEvent e) {
				// Calculate the change in mouse coordinates since last update.
				double deltax =   e.getX() - pickedPoint.x;
				double deltay = -(e.getY() - pickedPoint.y);
				
				if (shiftDown) {
					// When shift is pressed, rotate the shape.
					for (SceneGraphNode node : pickedNodes) {
						node.rotation += (deltax + deltay) / 2;
					}
					
				} else if (ctrlDown) {
					// When Ctrl is pressed, scale the shape.
					for (SceneGraphNode node : pickedNodes) {
						// Get distance from mouse to center of node.
						deltax = getMouseX(e) - node.translateX;
						deltay = getMouseY(e) - node.translateY;
						// Use Pythagorean theorem to get distance from node's center.
						node.scale = (float) Math.sqrt(deltax * deltax + deltay * deltay) * 2;
					}
					
				} else {
					
					// MouseEvent measures X/Y in pixels from top-left (0,0).
					// Translating this to JOGL's coordinates will require some fancy ratios.
					// right/left/top/bottom are in JOGL's coordinates, width/height are the component pixels.
					double coordPerPixelX = ((right - left) / width);
					double coordPerPixelY = ((top - bottom) / height); 
					
					// Translate all picked nodes
					for (SceneGraphNode node : pickedNodes) {
						node.translateX += (deltax * coordPerPixelX * (1 / SCALE));
						node.translateY += (deltay * coordPerPixelY * (1 / SCALE));
					}
				}
				
				// Remember this point for the next update.
				pickedPoint = new Point(e.getX(), e.getY());
			}
			
		});
		
		canvas.addMouseWheelListener(new MouseWheelListener() {
			public void mouseWheelMoved(MouseWheelEvent e) {
				int notches = e.getWheelRotation();
				if (notches > 0) {
					// Scroll down
					SCALE /= ((float) notches / 5) + 1;
				} else {
					// Scroll up
					SCALE *= ((float) Math.abs(notches) / 5) + 1;
				}
				sceneGraphRoot.scale = SCALE;
			}
			
		});
		sceneGraphRoot = new Scene();
		sceneGraphRoot.scale = SCALE;
		
		appFrame = new JFrame("Project 1");
		appFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
		appFrame.setMinimumSize(new Dimension(256, 256));
		appFrame.setJMenuBar(buildMenus());
		appFrame.add(canvas);
		appFrame.pack();
		if (Toolkit.getDefaultToolkit().isFrameStateSupported(JFrame.MAXIMIZED_BOTH))
			appFrame.setExtendedState(appFrame.getExtendedState() | JFrame.MAXIMIZED_BOTH);
		appFrame.setVisible(true);
	}
	
	// Convert mouse's X coordinate from pixels to JOGL's coordinate system.
	private float getMouseX(MouseEvent e) {
		float x = (float) (right - left)  * (e.getX() * 2 - (float) width) / ((float) (width));
		return x * (1 / SCALE) / 2;
	}
	
	// Convert mouse's Y coordinate from pixels to JOGL's coordinate system.
	private float getMouseY(MouseEvent e) {
		float y = (float) -(top - bottom) * (e.getY() * 2 - (float) height) / ((float) (height));
		return y * (1 / SCALE) / 2;
	}
	
	public void display(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();
		gl.glClear(GL.GL_COLOR_BUFFER_BIT);
		
		if (pickNextFrame) {
			GLU glu = GLU.createGLU(gl);
			int viewport[] = new int[4];
			gl.glGetIntegerv(GL.GL_VIEWPORT, viewport, 0);
			gl.glMatrixMode(GL2.GL_PROJECTION);
			gl.glPushMatrix();
			gl.glLoadIdentity();
			glu.gluPickMatrix(pickedPoint.x, (double)(viewport[3] - pickedPoint.y), 1, 1, viewport, 0);
			gl.glOrtho(left, right, bottom, top, -1, 1);
			gl.glMatrixMode(GL2.GL_MODELVIEW);
			pickedNodes = sceneGraphRoot.getPicked(drawable);
			gl.glMatrixMode(GL2.GL_PROJECTION);
			gl.glPopMatrix();
			gl.glMatrixMode(GL2.GL_MODELVIEW);
			pickNextFrame = false;
			
			// Now that we have the "picked" nodes, delete any if needed.
			if (deleteNextPicked) {
				for (SceneGraphNode node : pickedNodes) {
					sceneGraphRoot.removeChild(node);
				}
				deleteNextPicked = false;
			}
		}
		
		//this is an attempt to get some anti-aliasing (but I don't notice any difference)
		gl.glEnable(GL2.GL_POINT_SMOOTH);
		gl.glEnable(GL.GL_LINE_SMOOTH);
	    gl.glEnable(GL.GL_BLEND);
	    gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE_MINUS_SRC_ALPHA);
	    gl.glHint(GL.GL_LINE_SMOOTH_HINT, GL.GL_DONT_CARE);
	    
		sceneGraphRoot.render(drawable);
		// sceneGraphRoot.rotation++;
	}
	
	public void dispose(GLAutoDrawable drawable) {
	}
	
	public void init(GLAutoDrawable drawable) {
		GL2 gl = drawable.getGL().getGL2();
		gl.glClearColor(0, 0, 0, 0);
		
		IntBuffer selectBuffer = Buffers.newDirectIntBuffer(3);
		gl.glSelectBuffer(selectBuffer.capacity(), selectBuffer);
		
		FPSAnimator fps = new FPSAnimator(drawable, 30);
		fps.start();
	}
	
	public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
		final double UNIT = 2;
		
		if (width < height) {
			left = -UNIT / 2;
			right = UNIT / 2;
			top = (double)height / width * UNIT / 2;
			bottom = -top;
		} else {
			top = UNIT / 2;
			bottom = -UNIT / 2;
			right = (double)width / height * UNIT / 2;
			left = -right;
		}
		
		GL2 gl = drawable.getGL().getGL2();
		gl.glMatrixMode(GL2.GL_PROJECTION);
		gl.glLoadIdentity();
		gl.glOrtho(left, right, bottom, top, -1, 1);
		gl.glMatrixMode(GL2.GL_MODELVIEW);
		
		// Store height/width (in pixels!) for use in mouseDragged()
		Main.height = height;
		Main.width = width;
		
	}
}

